1. Introducción al notebook¶
Este notebook es la primera parte de una serie de cinco que constituye nuestra práctica para la asignatura de Visión Artificial del Máster en Inteligencia Artificial de la Universidad de Murcia. Este notebook expone la implementación de técnicas de procesamiento de imagen elementales para nuestro pipeline. Para el correcto funcionamiento del mismo es necesario instalar el paquete del proyecto siguiendo las instrucciones del repositorio de GitHub.
A lo largo del notebook, se explican las técnicas implementadas y se muestran ejemplos de su aplicación a imágenes de prueba. Se abordan técnicas como la corrección de distorsión de lentes, la conversión a escala de grises, el umbral de Otsu y el filtro CLAHE entre otras; definiendose más adelante su aplicabilidad en nuestro pipeline de visión artificial orientado a la detección de señales de tráfico.
Al final del documento, incluimos un apartado de conclusiones y justificación de los ítems de bloques cubiertos por nuestro trabajo. También dejamos un párrafo explicando el papel de la IA generativa en la elaboración del mismo.
2. Configuración e inicialización¶
2.1. Importación de librerías¶
En el siguiente fragmento de código se incluyen las importaciones necesarias para la ejecución de todos los bloques de código del notebook. Recuerde crear un entorno virtual con el proyecto instalado (Ejecutar "pip install -e ." en el directorio raíz del proyecto).
# Imports de python
import time
import statistics
# Librerías de terceros
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
# Librerías propias
from dgst.utils.loader import *
from dgst.utils.processor import *
2.2. Definición de funciones auxiliares¶
# Función para mostrar las imágenes en una cuadrícula
def plot_images(images, titles=None, grid_shape=(3, 4)):
fig, axes = plt.subplots(*grid_shape, figsize=(12, 9))
for i, ax in enumerate(axes.flat):
if i < len(images):
if (isinstance(images[i], np.ndarray)):
# If one channel, show as grayscale
if images[i].ndim == 2:
ax.imshow(images[i], cmap='gray')
else:
ax.imshow(images[i])
else:
image = images[i].get_image()
if (image.ndim == 2):
ax.imshow(image, cmap='gray')
else:
ax.imshow(cv.cvtColor(image, cv.COLOR_BGR2RGB))
if titles:
ax.set_title(titles[i])
else:
ax.set_title(f'Image {i}')
ax.axis('off')
plt.tight_layout()
plt.show()
3. Dataset e imágenes¶
3.1. Descripción del dataset e importación¶
En este apartado se describe el dataset utilizado en la práctica y se muestran algunas imágenes representativas del mismo. El dataset seleccionado es Zenseact Frames (ZOD) [1], que contiene imágenes de cámaras frontales de vehículos en diversas condiciones de iluminación y clima. Contiene 100.000 imágenes etiquetadas para tareas de detección y segmentación de objetos, así como datos de sensores adicionales como LiDAR y radar. Además, las imágenes han sido tomadas en 14 paises diferentes de la Unión Europea, lo que nos aporta un nuevo enfoque quizás más alejado de los datasets tradicionales tomados en Estados Unidos o Asia.
Dado que el dataset es muy grande, en este notebook se trabajará con un subconjunto reducido ubicado en /images/bt1. Además, dado que la estructura del dataset es muy regular, en lo que respecta a la organización de las imágenes junto a sus metadatos, se ha implementado un cargador que permite cargar las imágenes y sus etiquetas de forma sencilla dado su número de índice dentro del dataset y la raiz del mismo. A continuación, se muestran algunas imágenes representativas del dataset:
image_dir = './images/bt1'
data_loader = DataLoader(image_dir)
indices = [int(d) for d in os.listdir(image_dir) if d.isdigit()]
images = []
for idx in indices:
image = data_loader.load(idx)
images.append(image)
plot_images(images, grid_shape=(4, 3))
Loading image from ./images/bt1/000014/camera_front_blur/000014_india_2021-04-18T15:57:58.885517Z.jpg Loading image from ./images/bt1/000177/camera_front_blur/000177_golf_2021-04-26T09:01:16.995578Z.jpg Loading image from ./images/bt1/000205/camera_front_blur/000205_india_2021-04-29T13:26:40.774894Z.jpg Loading image from ./images/bt1/000247/camera_front_blur/000247_golf_2021-04-27T10:58:38.108810Z.jpg Loading image from ./images/bt1/020539/camera_front_blur/020539_india_2020-04-24T13:34:24.775134Z.jpg Loading image from ./images/bt1/020541/camera_front_blur/020541_india_2020-03-31T11:09:20.113995Z.jpg Loading image from ./images/bt1/020567/camera_front_blur/020567_india_2020-07-27T13:01:18.890907Z.jpg Loading image from ./images/bt1/020578/camera_front_blur/020578_oscar_2021-04-15T08:41:33.109881Z.jpg Loading image from ./images/bt1/020579/camera_front_blur/020579_india_2020-07-22T08:47:24.215579Z.jpg Loading image from ./images/bt1/020604/camera_front_blur/020604_golf_2021-02-23T14:04:48.112314Z.jpg Loading image from ./images/bt1/030352/camera_front_blur/030352_india_2020-10-14T18:44:23.000220Z.jpg
3.2. Visualización de regiones de interés¶
Además de cargar las imágenes, es importante visualizar las regiones de interés (ROIs) que contienen las señales de tráfico, esto nos servirá para verificar más adelante nuestro procesado de imágenes. Utilizando el cargador implementado, podemos cargar una imagen y sus ROIs asociadas, y luego dibujar las ROIs sobre la imagen para su visualización. A continuación, se muestra un ejemplo de cómo hacerlo:
clones = []
for image in images:
clones.append(image.clone())
clones[-1].show_rois()
plot_images(clones, grid_shape=(4, 3))
plot_images(clones, grid_shape=(1, 2))
Se puede observar como el etiquetado no es perfecto, ya que algunas señales no están completamente dentro de las regiones de interés (ROIs) definidas. Sin embargo, para los propósitos de esta práctica, consideraremos que las ROIs son suficientemente buenas para permitirnos más adelante analizar nuestra técnica de detección y reconocimiento de señales de tráfico.
4. Procesado de imágenes¶
En esta sección, cuando fuere necesario, utilizaremos $n$ y $m$ para referirnos a las dimensiones de una imagen y $k$ para el tamaño (lado) de un kernel de convolución.
4.1. La clase ImageProcessor¶
Para facilitar el procesado de imágenes, se ha implementado la clase ImageProcessor, que permite encadenar múltiples operaciones de procesado de imágenes de manera sencilla y modular. Esta clase utiliza el patrón de diseño "Builder", lo que permite añadir diferentes filtros y transformaciones a una imagen de forma fluida. A continuación, se muestra un ejemplo de cómo utilizar esta clase para aplicar una serie de transformaciones a una imagen:
# TODO: Añadir explicación aquí
4.2. Corrección de la distorsión¶
Obsérvese como la imagen original presenta la distorsión típica de una lente de ojo de pez. Como viene explicado en la web del dataset, estas cámaras pretenden maximizar el campo de visión de la cámara. Afortunadamente, el dataset incluye los parámetros de calibración de la cámara. Para corregir esta distorsión, es necesario aplicar el algoritmo de corrección de distorsión de Kannala-Brandt [2], que utiliza los parámetros de calibración para mapear los píxeles de la imagen distorsionada a sus posiciones correctas en una imagen sin distorsión. La implementación de este algoritmo implementado en C se encuentra en la función kannala_brandt_undistort del módulo dgst/filters/ffi/kannala.c. Tiene una complejidad espacial y temporal de $O(nm)$. A continuación, se muestra un ejemplo de cómo aplicar esta corrección a una imagen del dataset.
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_kannala_brandt_undistortion())
undistorted_image = processor.process(images[0])
undistorted_image.show_rois()
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Distorsionada"], grid_shape=(1, 2))
Obsérvese que, aun manteniendo la resolución original de la imagen, la distorsión ha sido corregida satisfactoriamente, sacrificando únicamente una pequeña parte de la imagen en los bordes. Véase también como el salpicadero del coche ya no se ve curvado y que las líneas de la carretera son rectas en lugar de curvas. Usamos nuestra correción de distorsión para tambien corregir las posiciones de las ROIs en la imagen, de forma que podamos seguir trabajando con ellas en el resto del pipeline.
4.3 Filtros básicos¶
4.3.1 Filtro de escala de grises¶
Tal y como su nombre indica, nuestro pipeline de procesamiento requerirá que la imagen se encuentre en escala de grises para poder aplicar ciertos filtros u operaciones. Para ello, en el procesador se pone a disposición el filtro de grises porporcionado por OpenCV.
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale())
undistorted_image = processor.process(images[0])
undistorted_image.show_rois()
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Escala de grises"], grid_shape=(1, 2))
4.3.2 Filtro de umbralización¶
El filtro de umbralización es una técnica de procesamiento de imágenes que convierte una imagen en escala de grises en una imagen binaria. Esto se logra estableciendo un umbral, de manera que todos los píxeles con valores por encima del umbral se establecen en blanco (255), y todos los píxeles con valores por debajo del umbral se establecen en negro (0). Este proceso es útil para resaltar características específicas de la imagen y facilitar su análisis. Nuestra implementación sencilla en C se encuentra en la carpeta dgst/filters/ffi/threshold.c. Se asume que la imagen de entrada ya está en blanco y negro. Tiene una complejidad de $O(nm)$
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale()
.add_threshold(threshold=0.5))
undistorted_image = processor.process(images[0])
undistorted_image.show_rois()
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Umbralizado"], grid_shape=(1, 2))
4.4. Filtros de suavizado¶
Nuestro pipeline requerirá de suavizado de imágenes para reducir el ruido y mejorar la calidad de los resultados.
4.4.1 Filtro de caja¶
El filtro de caja o de medias es un método simple y efectivo para suavizar imágenes. Este filtro reemplaza el valor de cada píxel por el promedio de los valores de los píxeles en su vecindario. Esto ayuda a reducir el ruido y los detalles finos en la imagen, lo que puede ser beneficioso para tareas de análisis de imágenes posteriores. Nuestra implementación en C se encuentra en la carpeta dgst/filters/ffi/box.c. Nos valemos de una optimización utilizando sumas parciales para incrementar el rendimiento y conseguir una complejidad de $O(nm)$ en tiempo y en memoria.
Se requiere que la imagen de entrada tenga una sola dimensión (blanco y negro). El tamaño del filtro proporcionado debe ser impar y mayor que cero.
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale()
.add_box_filter(filter_size=7))
undistorted_image = processor.process(images[0])
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Filtro de caja"], grid_shape=(1, 2))
4.4.2 Filtro gaussiano¶
El filtro gaussiano es otro método popular para el suavizado de imágenes. A diferencia del filtro de caja, que utiliza un promedio simple, el filtro gaussiano aplica una función de peso gaussiano a los píxeles en el vecindario de cada píxel. Esto significa que los píxeles más cercanos al píxel central tienen un mayor impacto en el valor suavizado que los píxeles más lejanos. Este enfoque ayuda a preservar los bordes y las características importantes de la imagen mientras se reduce el ruido. Nuestra implementación en C se encuentra en la carpeta dgst/filters/ffi/gaussian.c. Al igual que con el filtro de caja, se requiere que la imagen de entrada tenga una sola dimensión (blanco y negro). El tamaño del kernel gaussiano se calcula en función de la desviación estándar deseada siguiendo la relación $k=6\sigma+1$, con k siempre impar. Utilizando la propiedad de separabilidad de este filtro, conseguimos una complejidad de $O(nm\times(6\sigma+1))$ en espacio y memoria
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale()
.add_gaussian_filter(sigma=7))
undistorted_image = processor.process(images[0])
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Filtro Gaussiano"], grid_shape=(1, 2))
4.5 Filtros de detección de bordes¶
El objetivo general de este proyecto es la detección de señales de tráfico, lo que implicará detección de formas y por tanto, la necesidad de identificar los bordes de estas señales en la imagen. Para esto será necesario aplicar filtros de detección de bordes que resalten las transiciones en la intensidad de los píxeles.
4.5.1 Filtro de Canny¶
Este algoritmo se basa en una serie de pasos que incluyen la reducción de ruido, la detección de gradientes y la supresión de no-máximos, lo que permite identificar bordes de manera efectiva. La implementación en C se encuentra en la carpeta dgst/filters/ffi/canny.c. Al igual que con los filtros anteriores, se requiere que la imagen de entrada tenga una sola dimensión (blanco y negro). Además, necesita un suavizado previo para poder calcular los gradientes.
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale()
.add_gaussian_filter(sigma=0.33)
.add_canny_edge_detection(low_threshold=50, high_threshold=150))
undistorted_image = processor.process(images[0])
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Bordes de Canny"], grid_shape=(1, 2))
Obsérvese que hemos tenido que ajustar los parámetros del filtro gaussiano y de la detección de bordes de Canny para obtener mejores resultados. Estos ajustes son cruciales para optimizar el rendimiento del pipeline de procesamiento de imágenes. Se realizará un análisis más exhaustivo en la siguiente sección.
4.6 Filtros avanzados¶
En esta sección, exploraremos filtros más avanzados que pueden mejorar aún más la detección de bordes y la calidad de las imágenes. Hemos querido buscar técnicas más avanzadas para comparar su rendimiento con los filtros tradicionales que hemos utilizado hasta ahora.
4.6.1 Filtro de congruencia de fase¶
El filtro de congruencia de fase es una técnica avanzada que se utiliza para la detección de bordes y la mejora de imágenes. Este método se basa en la transformación de Fourier y se centra en la fase de la imagen en lugar de la amplitud. Al hacerlo, puede resaltar características importantes de la imagen que pueden no ser evidentes en el dominio espacial. En este caso, disponemos de 2 implementaciones, una en C y otra en Python con numpy, en los ficheros dgst/filters/ffi/phase.c y dgst/filters/python/phase.py respectivamente. Se mostrará la motivación y diferencia de rendimiento entre ambas implementaciones en la sección de análisis.
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale()
.add_gaussian_filter(sigma=1.4)
.add_phase_congruency(nscale=4, norient=6))
undistorted_image = processor.process(images[0])
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Congruencia de fase"], grid_shape=(1, 2))
Obsérvese como el filtro de congruencia es capaz de resaltar los bordes de forma muy efectiva, puediendose distinguir todos los contornos de la imagen, incluyendo las señales de tráfico que nos interesan. Combinándose con el filtro de umbralización, podemos obtener una imagen binaria que resalta los bordes detectados de manera muy clara.
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale()
.add_gaussian_filter(sigma=1.4)
.add_phase_congruency()
.add_threshold(threshold=0.96))
undistorted_image = processor.process(images[0])
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Bordes de fase"], grid_shape=(1, 2))
Uno de los inconvenientes es la generación de artefactos en zonas homogéneas de la imagen, lo que puede dificultar el análisis posterior. Sin embargo, estos artefactos pueden ser mitigados mediante el refinamiento de los parámetros del filtro y la aplicación de técnicas adicionales de post-procesamiento.
4.6.2 Filtro de umbralización automática de Otsu¶
El método de umbralización automática de Otsu es una técnica avanzada que permite determinar un umbral óptimo para convertir una imagen en escala de grises en una imagen binaria. A diferencia del umbral fijo, que requiere la selección manual de un valor, el método de Otsu analiza la distribución de los niveles de gris en la imagen y calcula el umbral que minimiza la varianza intra-clase, es decir, la varianza dentro de las regiones de fondo y primer plano. Nuestra implementación en Python se encuentra en el fichero dgst/filters/python/otsu.py utilizando OpenCV.
images_distortion = [images[0]]
processor = (ImageProcessor()
.add_grayscale()
.add_otsu_threshold())
undistorted_image = processor.process(images[0])
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "Umbralizado de Otsu"], grid_shape=(1, 2))
4.6.3. Filtro CLAHE (Contrast Limited Adaptive Histogram Equalization)¶
El filtro CLAHE (Contrast Limited Adaptive Histogram Equalization) es una técnica avanzada de mejora de contraste que se utiliza para mejorar la visibilidad de los detalles en imágenes con bajo contraste. A diferencia de la ecualización de histograma global, que puede amplificar el ruido en áreas homogéneas, CLAHE divide la imagen en pequeñas regiones (o "tiles") y aplica la ecualización de histograma a cada una de ellas de manera independiente. Además, limita el contraste para evitar la amplificación excesiva del ruido. Nuestra implementación en Python se encuentra en el fichero dgst/filters/python/clahe.py utilizando OpenCV.
images_distortion = [images[1]]
processor = (ImageProcessor()
.add_clahe())
undistorted_image = processor.process(images[1])
images_distortion.append(undistorted_image)
plot_images(images_distortion, titles=["Original", "CLAHE"], grid_shape=(1, 2))
En color se aprecia una mejora en la imagen pero no es tan evidente como en la imagen en escala de grises.
images_distortion = []
processor = (ImageProcessor()
.add_grayscale())
no_clahe = processor.process(images[1])
images_distortion.append(no_clahe)
p2 = (ImageProcessor()
.add_clahe()
.add_grayscale())
yes_clahe = p2.process(images[1])
images_distortion.append(yes_clahe)
plot_images(images_distortion, titles=["Gris", "CLAHE"], grid_shape=(1, 2))
5. Análisis y aplicación de filtros¶
Una vez expuestos los filtros sobre los que nos basamos, vamos a explorar como hacer combinaciones de los mismos para obtener los mejores resultados posibles en la tarea de detección de bordes. Empezaremos aplicando CLAHE a una imagen antes de realizar un umbralizado de Otsu y veremos como afecta esta modificación del contraste a la detección de elementos de la imagen.
processed_images = []
# No CLAHE.
no_clahe_processor = (ImageProcessor()
.add_grayscale()
.add_otsu_threshold())
# CLAHE.
clahe_processor = (ImageProcessor()
.add_clahe()
.add_grayscale()
.add_otsu_threshold())
for img in images[0:3]:
processed_images.append(no_clahe_processor.process(img))
processed_images.append(clahe_processor.process(img))
plot_images(processed_images, titles=["Gris", "CLAHE"]*3, grid_shape=(3, 2))
Apréciese como el contrate dinámico, aplicado por tiles locales en las imágenes, produce la suficiente distinción en los niveles de gris de las imágenes para permitir que el umbralizado de Otsu haga una mejor distinción de los elementos de la misma. El siguiente paso lógico es preguntarse si esto permite realizar una mejor detección de bordes en la imagen usando el clásico filtro de Canny. Para ello aplicaremos primero un suavizado gausiando para reducir el ruido, y luego aplicaremos Canny tanto a la imagen original como a la imagen procesada con CLAHE y con CLAHE y Otsu.
processed_images = []
# Canny standalone.
no_clahe_processor = (ImageProcessor()
.add_grayscale()
.add_gaussian_filter(sigma=1.4)
.add_canny_edge_detection(low_threshold=50, high_threshold=150)
)
# Canny con CLAHE.
only_clahe_processor = (ImageProcessor()
.add_clahe()
.add_grayscale()
.add_gaussian_filter(sigma=1.4)
.add_canny_edge_detection(low_threshold=50, high_threshold=150)
)
# Canny con OTSU.
otsu_processor = (ImageProcessor()
.add_grayscale()
.add_otsu_threshold()
.add_gaussian_filter(sigma=1.4)
.add_canny_edge_detection(low_threshold=50, high_threshold=150)
)
# Canny con CLAHE+OTSU.
clahe_otsu_processor = (ImageProcessor()
.add_clahe()
.add_grayscale()
.add_otsu_threshold()
.add_gaussian_filter(sigma=1.4)
.add_canny_edge_detection(low_threshold=50, high_threshold=150)
)
for img in images[0:3]:
processed_images.append(no_clahe_processor.process(img))
processed_images.append(only_clahe_processor.process(img))
processed_images.append(otsu_processor.process(img))
processed_images.append(clahe_otsu_processor.process(img))
plot_images(processed_images, titles=["CANNY", "CANNY+CLAHE", "CANNY+OTSU", "CANNY+CLAHE+OTSU"]*3, grid_shape=(3, 4))
Se observa como la aplicación de CLAHE mejora significativamente la detección de bordes en la imagen, resaltando las señales de tráfico y otros elementos importantes. La combinación de CLAHE con el umbralizado de Otsu también muestra mejoras, aunque en este caso, la diferencia no es tan pronunciada como con CLAHE solo e introduce mucho ruido que en íiertas situaciones podría dificultar la detección de elementos. Para reducirlo una opción es incrementar el suavizado gaussiano previo a la detección de bordes.
Mirando en otra dirección, tambien es interesante explorar el rendimiento del filtro de congruencia de fase en comparación con los métodos tradicionales. Aplicando este filtro a la imagen original y a la imagen procesada con CLAHE, podemos observar cómo resalta los bordes de manera efectiva. Para ello, aplicamos un suavizado gaussiano previo para reducir el ruido y luego aplicamos el filtro de congruencia de fase y un umbralizado para obtener una imagen binaria de los bordes detectados.
processed_images = []
# No CLAHE.
no_clahe_processor = (ImageProcessor()
.add_grayscale()
.add_gaussian_filter(sigma=1.4)
.add_phase_congruency()
.add_threshold(threshold=0.96))
# CLAHE.
clahe_processor = (ImageProcessor()
.add_clahe()
.add_grayscale()
.add_gaussian_filter(sigma=1.4)
.add_phase_congruency()
.add_threshold(threshold=0.96))
for img in images[0:3]:
processed_images.append(no_clahe_processor.process(img))
processed_images.append(clahe_processor.process(img))
plot_images(processed_images, titles=["No CLAHE", "CLAHE"]*3, grid_shape=(3, 2))
Curiosamente, utilizando CLAHE empeoran ligeramente los resultados al introducirse ruido adicional en la imagen. Esto se debe a que CLAHE modifica los niveles de gris de la imagen de manera local, lo que puede afectar negativamente al cálculo de la congruencia de fase. Por tanto, en este caso, parece que el filtro de congruencia de fase funciona mejor con la imagen original suavizada.
Vamos ahora a comparar cualitativamente los resultados obtenidos con los diferentes métodos de detección de bordes que hemos explorado. Para ello, aplicaremos cada método a un conjunto de imágenes del dataset y visualizaremos los resultados para compararlos.
processed_images = []
# Congruencia de fase.
phase_processor = (ImageProcessor()
.add_kannala_brandt_undistortion()
.add_grayscale()
.add_gaussian_filter(sigma=1.4)
.add_phase_congruency()
.add_threshold(threshold=0.96))
# Canny con CLAHE.
canny_processor = (ImageProcessor()
.add_kannala_brandt_undistortion()
.add_clahe()
.add_grayscale()
.add_otsu_threshold()
.add_gaussian_filter(sigma=2.8)
.add_canny_edge_detection(low_threshold=50, high_threshold=150)
)
for img in images[0:3]:
processed_images.append(img)
processed_images.append(phase_processor.process(img))
processed_images.append(canny_processor.process(img))
plot_images(processed_images, titles=["Original", "Phase", "Canny"]*3, grid_shape=(3, 3))
plot_images(processed_images[1:3], titles=["Phase", "Canny"], grid_shape=(1, 2))
En una comparativa entre nuestro filtro de congruencia de fase y el filtro de Canny con CLAHE, podemos observar que ambos métodos son capaces de detectar los bordes de manera efectiva. Sin embargo, el filtro de congruencia de fase tiende a resaltar más detalles finos y bordes débiles, mientras que el filtro de Canny con CLAHE y Otsu proporciona una detección más robusta y menos sensible al ruido, habiendo reducido ese ruido introducido por la aplicación de Otsu mediante el incremento de la desviación en el filtro Gausiano previo a la detección de bordes. La idoneidad de cada método dependerá del contexto específico en el que usemos las imágenes.
6. Rendimientos y comparativas¶
6.1. Filtros de suavizado¶
Vamos a realizar un pequeño análissis de rendimiento de las diferentes implementaciones de los filtros que hemos desarrollado. En particular, nos centraremos en comparar el rendimiento de la implementación en C de los filtros de suavizado.
TEST_DENIS_PATH = "./images/test/denis.jpg"
img_denis = cv2.imread(TEST_DENIS_PATH, cv2.IMREAD_GRAYSCALE)
TEST_LENNA_PATH = "./images/test/lenna.png"
img_lenna = cv2.imread(TEST_LENNA_PATH, cv2.IMREAD_GRAYSCALE)
# Storage for benchmark results
benchmark_results = {
'box': {'ffi': {}, 'cv': {}},
'gaussian': {'ffi': {}, 'cv': {}}
}
# --------------------------------------------------------------
# Benchmark box_filter.
# --------------------------------------------------------------
def box_filter_benchmark_ffi(image, filter_size, iterations=10):
times = []
for _ in range(iterations):
start_time = time.time()
output = box_filter(image, filter_size)
end_time = time.time()
times.append((end_time - start_time) * 1000) # Convert to milliseconds
mean_time = statistics.mean(times)
std_dev = statistics.stdev(times) if len(times) > 1 else 0.0
print(
f"Box Filter FFI (size={filter_size}) - Mean: {mean_time:.3f} ms, Std Dev: {std_dev:.3f} ms ({iterations} iterations)"
)
return mean_time, std_dev
def box_filter_benchmark_cv(image, filter_size, iterations=10):
times = []
for _ in range(iterations):
start_time = time.time()
kernel = np.ones((filter_size, filter_size), np.float32) / (
filter_size * filter_size
)
filtered_cv = cv2.filter2D(image, -1, kernel)
end_time = time.time()
times.append((end_time - start_time) * 1000) # Convert to milliseconds
mean_time = statistics.mean(times)
std_dev = statistics.stdev(times) if len(times) > 1 else 0.0
print(
f"Box Filter CV (size={filter_size}) - Mean: {mean_time:.3f} ms, Std Dev: {std_dev:.3f} ms ({iterations} iterations)"
)
return mean_time, std_dev
# --------------------------------------------------------------
# Benchmark gaussian_filter.
# --------------------------------------------------------------
def gaussian_filter_benchmark_ffi(image, sigma, iterations=10):
times = []
for _ in range(iterations):
start_time = time.time()
output = gaussian_filter(image, sigma)
end_time = time.time()
times.append((end_time - start_time) * 1000) # Convert to milliseconds
mean_time = statistics.mean(times)
std_dev = statistics.stdev(times) if len(times) > 1 else 0.0
print(
f"Gaussian Filter FFI (sigma={sigma}) - Mean: {mean_time:.3f} ms, Std Dev: {std_dev:.3f} ms ({iterations} iterations)"
)
return mean_time, std_dev
def gaussian_filter_benchmark_cv(image, sigma, iterations=10):
times = []
for _ in range(iterations):
start_time = time.time()
filtered_cv = cv2.GaussianBlur(image, (0, 0), sigmaX=sigma)
end_time = time.time()
times.append((end_time - start_time) * 1000) # Convert to milliseconds
mean_time = statistics.mean(times)
std_dev = statistics.stdev(times) if len(times) > 1 else 0.0
print(
f"Gaussian Filter CV (sigma={sigma}) - Mean: {mean_time:.3f} ms, Std Dev: {std_dev:.3f} ms ({iterations} iterations)"
)
return mean_time, std_dev
box_filter_sizes = [3, 5, 7, 9, 11, 15]
gaussian_filter_sigmas = [0.5, 1.0, 1.5, 2.0, 2.5, 11.0, 15.0]
iterations = 1000
imgs = [img_lenna, img_denis]
img_names = ['Lenna (512x512)', 'Denis (1920x1200)']
for img, img_name in zip(imgs, img_names):
if img is None:
print("Error: Test image not found or could not be loaded.")
continue
img_key = img_name
benchmark_results['box']['ffi'][img_key] = []
benchmark_results['box']['cv'][img_key] = []
benchmark_results['gaussian']['ffi'][img_key] = []
benchmark_results['gaussian']['cv'][img_key] = []
print(f"\nBenchmarking on image: {img_name} - shape: {img.shape}\n")
print("Benchmarking Box Filter:")
for size in box_filter_sizes:
mean_ffi, std_ffi = box_filter_benchmark_ffi(img, size, iterations)
mean_cv, std_cv = box_filter_benchmark_cv(img, size, iterations)
benchmark_results['box']['ffi'][img_key].append((size, mean_ffi, std_ffi))
benchmark_results['box']['cv'][img_key].append((size, mean_cv, std_cv))
print("\nBenchmarking Gaussian Filter:")
for sigma in gaussian_filter_sigmas:
mean_ffi, std_ffi = gaussian_filter_benchmark_ffi(img, sigma, iterations)
mean_cv, std_cv = gaussian_filter_benchmark_cv(img, sigma, iterations)
benchmark_results['gaussian']['ffi'][img_key].append((sigma, mean_ffi, std_ffi))
benchmark_results['gaussian']['cv'][img_key].append((sigma, mean_cv, std_cv))
Benchmarking on image: Lenna (512x512) - shape: (512, 512) Benchmarking Box Filter: Box Filter FFI (size=3) - Mean: 1.198 ms, Std Dev: 0.707 ms (1000 iterations) Box Filter CV (size=3) - Mean: 0.243 ms, Std Dev: 0.117 ms (1000 iterations) Box Filter FFI (size=5) - Mean: 0.961 ms, Std Dev: 0.155 ms (1000 iterations) Box Filter CV (size=5) - Mean: 0.620 ms, Std Dev: 0.114 ms (1000 iterations) Box Filter FFI (size=7) - Mean: 1.028 ms, Std Dev: 0.247 ms (1000 iterations) Box Filter CV (size=7) - Mean: 1.138 ms, Std Dev: 0.142 ms (1000 iterations) Box Filter FFI (size=9) - Mean: 1.068 ms, Std Dev: 0.253 ms (1000 iterations) Box Filter CV (size=9) - Mean: 1.879 ms, Std Dev: 0.303 ms (1000 iterations) Box Filter FFI (size=11) - Mean: 0.947 ms, Std Dev: 0.084 ms (1000 iterations) Box Filter CV (size=11) - Mean: 2.633 ms, Std Dev: 0.320 ms (1000 iterations) Box Filter FFI (size=15) - Mean: 0.943 ms, Std Dev: 0.159 ms (1000 iterations) Box Filter CV (size=15) - Mean: 3.799 ms, Std Dev: 0.504 ms (1000 iterations) Benchmarking Gaussian Filter: Gaussian Filter FFI (sigma=0.5) - Mean: 3.731 ms, Std Dev: 2.829 ms (1000 iterations) Gaussian Filter CV (sigma=0.5) - Mean: 0.285 ms, Std Dev: 0.661 ms (1000 iterations) Gaussian Filter FFI (sigma=1.0) - Mean: 4.828 ms, Std Dev: 3.553 ms (1000 iterations) Gaussian Filter CV (sigma=1.0) - Mean: 0.330 ms, Std Dev: 0.472 ms (1000 iterations) Gaussian Filter FFI (sigma=1.5) - Mean: 5.301 ms, Std Dev: 3.757 ms (1000 iterations) Gaussian Filter CV (sigma=1.5) - Mean: 0.421 ms, Std Dev: 0.408 ms (1000 iterations) Gaussian Filter FFI (sigma=2.0) - Mean: 3.872 ms, Std Dev: 3.005 ms (1000 iterations) Gaussian Filter CV (sigma=2.0) - Mean: 0.482 ms, Std Dev: 0.377 ms (1000 iterations) Gaussian Filter FFI (sigma=2.5) - Mean: 5.157 ms, Std Dev: 3.439 ms (1000 iterations) Gaussian Filter CV (sigma=2.5) - Mean: 0.783 ms, Std Dev: 0.600 ms (1000 iterations) Gaussian Filter FFI (sigma=11.0) - Mean: 14.894 ms, Std Dev: 5.660 ms (1000 iterations) Gaussian Filter CV (sigma=11.0) - Mean: 6.873 ms, Std Dev: 1.938 ms (1000 iterations) Gaussian Filter FFI (sigma=15.0) - Mean: 18.120 ms, Std Dev: 5.056 ms (1000 iterations) Gaussian Filter CV (sigma=15.0) - Mean: 11.960 ms, Std Dev: 2.072 ms (1000 iterations) Benchmarking on image: Denis (1920x1200) - shape: (1200, 800) Benchmarking Box Filter: Box Filter FFI (size=3) - Mean: 3.418 ms, Std Dev: 0.325 ms (1000 iterations) Box Filter CV (size=3) - Mean: 0.888 ms, Std Dev: 0.325 ms (1000 iterations) Box Filter FFI (size=5) - Mean: 3.582 ms, Std Dev: 0.476 ms (1000 iterations) Box Filter CV (size=5) - Mean: 1.965 ms, Std Dev: 0.249 ms (1000 iterations) Box Filter FFI (size=7) - Mean: 3.578 ms, Std Dev: 0.401 ms (1000 iterations) Box Filter CV (size=7) - Mean: 3.832 ms, Std Dev: 0.509 ms (1000 iterations) Box Filter FFI (size=9) - Mean: 3.448 ms, Std Dev: 0.382 ms (1000 iterations) Box Filter CV (size=9) - Mean: 6.089 ms, Std Dev: 0.558 ms (1000 iterations) Box Filter FFI (size=11) - Mean: 3.803 ms, Std Dev: 0.850 ms (1000 iterations) Box Filter CV (size=11) - Mean: 9.646 ms, Std Dev: 1.514 ms (1000 iterations) Box Filter FFI (size=15) - Mean: 3.933 ms, Std Dev: 1.134 ms (1000 iterations) Box Filter CV (size=15) - Mean: 8.864 ms, Std Dev: 0.539 ms (1000 iterations) Benchmarking Gaussian Filter: Gaussian Filter FFI (sigma=0.5) - Mean: 6.893 ms, Std Dev: 4.266 ms (1000 iterations) Gaussian Filter CV (sigma=0.5) - Mean: 0.439 ms, Std Dev: 0.412 ms (1000 iterations) Gaussian Filter FFI (sigma=1.0) - Mean: 9.025 ms, Std Dev: 4.934 ms (1000 iterations) Gaussian Filter CV (sigma=1.0) - Mean: 0.652 ms, Std Dev: 0.528 ms (1000 iterations) Gaussian Filter FFI (sigma=1.5) - Mean: 11.229 ms, Std Dev: 5.196 ms (1000 iterations) Gaussian Filter CV (sigma=1.5) - Mean: 0.970 ms, Std Dev: 0.610 ms (1000 iterations) Gaussian Filter FFI (sigma=2.0) - Mean: 11.717 ms, Std Dev: 5.571 ms (1000 iterations) Gaussian Filter CV (sigma=2.0) - Mean: 1.187 ms, Std Dev: 0.735 ms (1000 iterations) Gaussian Filter FFI (sigma=2.5) - Mean: 13.749 ms, Std Dev: 5.441 ms (1000 iterations) Gaussian Filter CV (sigma=2.5) - Mean: 1.439 ms, Std Dev: 0.750 ms (1000 iterations) Gaussian Filter FFI (sigma=11.0) - Mean: 39.833 ms, Std Dev: 7.914 ms (1000 iterations) Gaussian Filter CV (sigma=11.0) - Mean: 12.912 ms, Std Dev: 2.840 ms (1000 iterations) Gaussian Filter FFI (sigma=15.0) - Mean: 50.254 ms, Std Dev: 9.834 ms (1000 iterations) Gaussian Filter CV (sigma=15.0) - Mean: 20.813 ms, Std Dev: 3.321 ms (1000 iterations)
# Visualización de resultados de benchmark
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# Box Filter - Lenna
ax = axes[0, 0]
lenna_key = img_names[0]
sizes = [x[0] for x in benchmark_results['box']['ffi'][lenna_key]]
times_ffi = [x[1] for x in benchmark_results['box']['ffi'][lenna_key]]
times_cv = [x[1] for x in benchmark_results['box']['cv'][lenna_key]]
std_ffi = [x[2] for x in benchmark_results['box']['ffi'][lenna_key]]
std_cv = [x[2] for x in benchmark_results['box']['cv'][lenna_key]]
ax.errorbar(sizes, times_ffi, yerr=std_ffi, marker='o', label='FFI (C)', capsize=5, linewidth=2, markersize=8)
ax.errorbar(sizes, times_cv, yerr=std_cv, marker='s', label='OpenCV', capsize=5, linewidth=2, markersize=8)
ax.set_xlabel('Filter Size', fontsize=12)
ax.set_ylabel('Execution Time (ms)', fontsize=12)
ax.set_title(f'Box Filter - {lenna_key}', fontsize=14, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Box Filter - Denis
ax = axes[0, 1]
denis_key = img_names[1]
sizes = [x[0] for x in benchmark_results['box']['ffi'][denis_key]]
times_ffi = [x[1] for x in benchmark_results['box']['ffi'][denis_key]]
times_cv = [x[1] for x in benchmark_results['box']['cv'][denis_key]]
std_ffi = [x[2] for x in benchmark_results['box']['ffi'][denis_key]]
std_cv = [x[2] for x in benchmark_results['box']['cv'][denis_key]]
ax.errorbar(sizes, times_ffi, yerr=std_ffi, marker='o', label='FFI (C)', capsize=5, linewidth=2, markersize=8)
ax.errorbar(sizes, times_cv, yerr=std_cv, marker='s', label='OpenCV', capsize=5, linewidth=2, markersize=8)
ax.set_xlabel('Filter Size', fontsize=12)
ax.set_ylabel('Execution Time (ms)', fontsize=12)
ax.set_title(f'Box Filter - {denis_key}', fontsize=14, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Gaussian Filter - Lenna
ax = axes[1, 0]
sigmas = [x[0] for x in benchmark_results['gaussian']['ffi'][lenna_key]]
times_ffi = [x[1] for x in benchmark_results['gaussian']['ffi'][lenna_key]]
times_cv = [x[1] for x in benchmark_results['gaussian']['cv'][lenna_key]]
std_ffi = [x[2] for x in benchmark_results['gaussian']['ffi'][lenna_key]]
std_cv = [x[2] for x in benchmark_results['gaussian']['cv'][lenna_key]]
ax.errorbar(sigmas, times_ffi, yerr=std_ffi, marker='o', label='FFI (C)', capsize=5, linewidth=2, markersize=8)
ax.errorbar(sigmas, times_cv, yerr=std_cv, marker='s', label='OpenCV', capsize=5, linewidth=2, markersize=8)
ax.set_xlabel('Sigma', fontsize=12)
ax.set_ylabel('Execution Time (ms)', fontsize=12)
ax.set_title(f'Gaussian Filter - {lenna_key}', fontsize=14, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Gaussian Filter - Denis
ax = axes[1, 1]
sigmas = [x[0] for x in benchmark_results['gaussian']['ffi'][denis_key]]
times_ffi = [x[1] for x in benchmark_results['gaussian']['ffi'][denis_key]]
times_cv = [x[1] for x in benchmark_results['gaussian']['cv'][denis_key]]
std_ffi = [x[2] for x in benchmark_results['gaussian']['ffi'][denis_key]]
std_cv = [x[2] for x in benchmark_results['gaussian']['cv'][denis_key]]
ax.errorbar(sigmas, times_ffi, yerr=std_ffi, marker='o', label='FFI (C)', capsize=5, linewidth=2, markersize=8)
ax.errorbar(sigmas, times_cv, yerr=std_cv, marker='s', label='OpenCV', capsize=5, linewidth=2, markersize=8)
ax.set_xlabel('Sigma', fontsize=12)
ax.set_ylabel('Execution Time (ms)', fontsize=12)
ax.set_title(f'Gaussian Filter - {denis_key}', fontsize=14, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Calcular y mostrar speedups
print("\n" + "="*80)
print("ANÁLISIS DE SPEEDUP (FFI vs OpenCV)")
print("="*80)
for img_name in img_names:
print(f"\n{img_name}:")
print("-" * 60)
print("\nBox Filter:")
for i, size in enumerate(box_filter_sizes):
ffi_time = benchmark_results['box']['ffi'][img_name][i][1]
cv_time = benchmark_results['box']['cv'][img_name][i][1]
speedup = cv_time / ffi_time
status = "FFI wins" if speedup > 1 else "OpenCV wins"
print(f" Size {size:2d}: Speedup = {speedup:.2f}x ({status})")
print("\nGaussian Filter:")
for i, sigma in enumerate(gaussian_filter_sigmas):
ffi_time = benchmark_results['gaussian']['ffi'][img_name][i][1]
cv_time = benchmark_results['gaussian']['cv'][img_name][i][1]
speedup = cv_time / ffi_time
status = "FFI wins" if speedup > 1 else "OpenCV wins"
print(f" Sigma {sigma:4.1f}: Speedup = {speedup:.2f}x ({status})")
================================================================================ ANÁLISIS DE SPEEDUP (FFI vs OpenCV) ================================================================================ Lenna (512x512): ------------------------------------------------------------ Box Filter: Size 3: Speedup = 0.20x (OpenCV wins) Size 5: Speedup = 0.65x (OpenCV wins) Size 7: Speedup = 1.11x (FFI wins) Size 9: Speedup = 1.76x (FFI wins) Size 11: Speedup = 2.78x (FFI wins) Size 15: Speedup = 4.03x (FFI wins) Gaussian Filter: Sigma 0.5: Speedup = 0.08x (OpenCV wins) Sigma 1.0: Speedup = 0.07x (OpenCV wins) Sigma 1.5: Speedup = 0.08x (OpenCV wins) Sigma 2.0: Speedup = 0.12x (OpenCV wins) Sigma 2.5: Speedup = 0.15x (OpenCV wins) Sigma 11.0: Speedup = 0.46x (OpenCV wins) Sigma 15.0: Speedup = 0.66x (OpenCV wins) Denis (1920x1200): ------------------------------------------------------------ Box Filter: Size 3: Speedup = 0.26x (OpenCV wins) Size 5: Speedup = 0.55x (OpenCV wins) Size 7: Speedup = 1.07x (FFI wins) Size 9: Speedup = 1.77x (FFI wins) Size 11: Speedup = 2.54x (FFI wins) Size 15: Speedup = 2.25x (FFI wins) Gaussian Filter: Sigma 0.5: Speedup = 0.06x (OpenCV wins) Sigma 1.0: Speedup = 0.07x (OpenCV wins) Sigma 1.5: Speedup = 0.09x (OpenCV wins) Sigma 2.0: Speedup = 0.10x (OpenCV wins) Sigma 2.5: Speedup = 0.10x (OpenCV wins) Sigma 11.0: Speedup = 0.32x (OpenCV wins) Sigma 15.0: Speedup = 0.41x (OpenCV wins)
Dado que aplicamos una optimización de sumas parciales, para tamaños pequeños de filtro, obtenemos un rendimiento hasta de 0.6x respecto a OpenCV. No obstante, al incrementar el tamaño del filtro el rendimiento en OpenCV se degrada y gana nuestra implementación, obtienendo hasta un 3x en imágenes pequeñas. Sin embargo, en el filtro Gaussiano, debemos conceder la victoria (por el momento) a OpenCV, ya que internamente harán uso de algoritmos más complejos como transformadas de Fourier, que debido al alcance de esta asignatura, no hemos implementado. Por tanto OpenCV obtiene un mejor rendimiento en la mayoría, o totalidad según el hardware, de casos.
También es necesario ver la diferencia cualitativa entre los suavizados para ver si la implementación en C es correcta. Como se observará, las diferencias son mínimas y se deben principalmente a diferencias internas en el manejo de bordes y algoritmos.
# Box filter comparison
box_ffi = box_filter(img_lenna, filter_size=11)
kernel = np.ones((11, 11), np.float32) / (11 * 11)
box_cv = cv2.filter2D(img_lenna, -1, kernel)
diff = cv2.absdiff(box_ffi, box_cv)
# Gaussian filter comparison
gaussian_ffi = gaussian_filter(img_lenna, sigma=11.0)
gaussian_cv = cv2.GaussianBlur(img_lenna, (0, 0), sigmaX=11.0)
diff_gaussian = cv2.absdiff(gaussian_ffi, gaussian_cv)
plt.figure(figsize=(12, 8))
plt.subplot(2, 3, 1)
plt.title("Box Filter FFI")
plt.imshow(box_ffi, cmap='gray')
plt.axis('off')
plt.subplot(2, 3, 2)
plt.title("Box Filter OpenCV")
plt.imshow(box_cv, cmap='gray')
plt.axis('off')
plt.subplot(2, 3, 3)
plt.title("Box Filter Difference")
plt.imshow(diff, cmap='gray')
plt.axis('off')
plt.subplot(2, 3, 4)
plt.title("Gaussian Filter FFI")
plt.imshow(gaussian_ffi, cmap='gray')
plt.axis('off')
plt.subplot(2, 3, 5)
plt.title("Gaussian Filter OpenCV")
plt.imshow(gaussian_cv, cmap='gray')
plt.axis('off')
plt.subplot(2, 3, 6)
plt.title("Gaussian Filter Difference")
plt.imshow(diff_gaussian, cmap='gray')
plt.axis('off')
plt.tight_layout()
plt.show()
6.2. Filtro de congruencia de fase¶
Respecto a la implementación del filtro de congruencia de fase en C frente a la implementación en Python con numpy, nuestros experimentos muestran que la versión en C es abismalmente mas lenta que la versión de Python, por lo que omitiremos su uso en favor de la versión en Python, que ya se ha mostrado en secciones anteriores.
6.3. Corrección de distorsión con Kannala-Brandt¶
No hemos implementado una versión en Python del filtro de corrección de distorsión de Kannala-Brandt, por lo que no podemos realizar una comparación directa de rendimiento.
6.4. Filtro de Canny¶
Al igual que con el suavizado, hemos comparado el rendimiento de nuestra implementación en C del filtro de Canny con la implementación de OpenCV.
def canny_filter_benchmark_ffi(image, low_threshold, high_threshold, iterations=10):
times = []
for _ in range(iterations):
start_time = time.time()
output = canny_edge_detection(image, low_threshold, high_threshold)
end_time = time.time()
times.append((end_time - start_time) * 1000) # Convert to milliseconds
mean_time = statistics.mean(times)
std_dev = statistics.stdev(times) if len(times) > 1 else 0.0
print(
f"Canny Filter FFI (low={low_threshold}, high={high_threshold}) - Mean: {mean_time:.3f} ms, Std Dev: {std_dev:.3f} ms ({iterations} iterations)"
)
return mean_time, std_dev
def canny_filter_benchmark_cv(image, low_threshold, high_threshold, iterations=10):
times = []
for _ in range(iterations):
start_time = time.time()
filtered_cv = cv2.Canny(image, low_threshold, high_threshold)
end_time = time.time()
times.append((end_time - start_time) * 1000) # Convert to milliseconds
mean_time = statistics.mean(times)
std_dev = statistics.stdev(times) if len(times) > 1 else 0.0
print(
f"Canny Filter CV (low={low_threshold}, high={high_threshold}) - Mean: {mean_time:.3f} ms, Std Dev: {std_dev:.3f} ms ({iterations} iterations)"
)
return mean_time, std_dev
# Canny benchmark
canny_low_threshold = 50
canny_high_threshold = 150
iterations = 1000
mean_ffi, std_ffi = canny_filter_benchmark_ffi(img_lenna, canny_low_threshold, canny_high_threshold, iterations)
mean_cv, std_cv = canny_filter_benchmark_cv(img_lenna, canny_low_threshold, canny_high_threshold, iterations)
Canny Filter FFI (low=50, high=150) - Mean: 6.828 ms, Std Dev: 3.049 ms (1000 iterations) Canny Filter CV (low=50, high=150) - Mean: 0.698 ms, Std Dev: 0.554 ms (1000 iterations)
Como era de esperar, OpenCV obtiene un mejor rendimiento, pero consideramos nuestra implementación suficientemente rápida para los propósitos de este proyecto.
Cualitativamente, los resultados obtenidos con nuestra implementación son comparables a los de OpenCV, aunque pueden existir pequeñas diferencias debido a la elección de parámetros, suavizado previo y detalles de implementación. Por lo general, la implementación de OpenCV parece capaz de detectar bordes ligeramente más finos y precisos.
canny_ffi = canny_edge_detection(img_lenna, canny_low_threshold, canny_high_threshold)
canny_cv = cv2.Canny(img_lenna, canny_low_threshold, canny_high_threshold)
diff_canny = cv2.absdiff(canny_ffi, canny_cv)
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.title("Canny Filter FFI")
plt.imshow(canny_ffi, cmap='gray')
plt.axis('off')
plt.subplot(1, 3, 2)
plt.title("Canny Filter OpenCV")
plt.imshow(canny_cv, cmap='gray')
plt.axis('off')
plt.subplot(1, 3, 3)
plt.title("Canny Filter Difference")
plt.imshow(diff_canny, cmap='gray')
plt.axis('off')
plt.tight_layout()
plt.show()
7. Conclusiones¶
Hemos explorado diversas técnicas de procesamiento de imágenes con el objetivo de mejorar la detección de bordes en imágenes de tráfico. A través de la implementación y análisis de filtros como CLAHE, umbralización de Otsu, filtro de Canny y congruencia de fase, hemos podido evaluar sus fortalezas y debilidades en diferentes combinaciones. A partir de nuestros experimentos, hemos concluido que la elección del filtro y su configuración dependen en gran medida del contexto específico de las imágenes y los objetivos del análisis. En general, la combinación de CLAHE con Canny ha demostrado ser efectiva para resaltar bordes importantes, mientras que la congruencia de fase ofrece una alternativa prometedora para la detección de detalles finos.
Hemos cubierto los siguientes ítems:
- BT1d - Implementación de filtros de procesamiento de imágenes básicos y avanzados.
- BT1e - Implementación manual de algoritmos de procesamiento de imágenes en C y Python.
- BT1h - Rectificación planar. Deshacer la distorsión de la lente ojo de pez.
- BT1o - Umbralizado dinámico con Otsu y CLAHE.
- BT1p - Gráficas de resultados y comparativas de rendimiento.
Uso de IA¶
Para la elaboración de este trabajo se ha utilizado IA conversacional para asistir en la elaboración de código y entendimiento de los algoritmos implementados.
Bibliografía¶
[1] Zenseact Frames Dataset. https://zod.zenseact.com/frames/
[2] J. Kannala and S. Brandt, "A generic camera calibration method for fish-eye lenses," 2004 IEEE International Conference on Pattern Recognition (ICPR), 2004, pp. 10-13 Vol.1. DOI: https://doi.org/10.1109/ICPR.2004.1333993